Micron Document
🎖️GitЯра🎖️

Commit 2dba958e1e633f2ad76913f726694eb2ec9d65ec


Parents : fef124b
Author : Jeremiah K <17190268+jeremiah-k@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-06-29T11:17:19-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-06-29T16:17:19Z

fix(qr): Preserve incoming channels when adding from QR (#6013)

Changes
Diff

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
index ed5f8c9f92..239f4f6ff9 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
@@ -63,6 +63,7 @@ import org.meshtastic.core.resources.replace
import org.meshtastic.core.resources.replace_channels_and_settings_description
import org.meshtastic.core.ui.component.ChannelSelection
import org.meshtastic.core.ui.theme.AppTheme
+import org.meshtastic.core.ui.util.mergeChannelSettingsForAdd
import org.meshtastic.proto.ChannelSet
@Composable
@@ -107,9 +108,7 @@ fun ScannedQrCodeDialog(
),
)
} else {
- // To guarantee consistent ordering, using a LinkedHashSet which iterates through
- // its entries according to the order an item was *first* inserted.
- val result = (channels.settings + incoming.settings).distinct()
+ val result = mergeChannelSettingsForAdd(channels.settings, incoming.settings)
channels.copy(settings = result)
}
}

diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt
index 5925e39e32..4a2e1d380f 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt
@@ -154,3 +154,20 @@ suspend fun applyReplacementChannelSet(
}
radioConfigRepository.replaceAllSettings(channelSet.settings)
}
+
+/**
+ * Builds the ADD-mode preview list for QR import. Existing channels are preserved at their positions; every incoming
+ * channel is appended in order without deduplication.
+ *
+ * Structural `.distinct()` was previously used here, but it silently dropped incoming channels that matched existing
+ * entries, shifting later channels to wrong indices and hiding them from the user. The caller (UI) lets the user select
+ * which incoming channels to keep.
+ *
+ * @param existing The current [ChannelSettings] list on the radio. Preserved in order.
+ * @param incoming The imported [ChannelSettings] list. Appended in order.
+ * @return The concatenated list `[existing..., incoming...]`.
+ */
+fun mergeChannelSettingsForAdd(
+ existing: List<ChannelSettings>,
+ incoming: List<ChannelSettings>,
+): List<ChannelSettings> = existing + incoming

diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt
index 4afc97923e..bf6a55dcfd 100644
--- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt
+++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/util/ProtoExtensionsTest.kt
@@ -141,4 +141,75 @@ class ProtoExtensionsTest {
assertEquals(2, result[2].index)
assertEquals(secondaryB, result[2].settings)
}
+
+ // --- mergeChannelSettingsForAdd tests ---
+
+ @Test
+ fun merge_preserves_all_existing_channels_in_order() {
+ val existing = listOf(ChannelSettings(name = "A"), ChannelSettings(name = "B"))
+
+ val result = mergeChannelSettingsForAdd(existing, incoming = emptyList())
+
+ assertEquals(2, result.size)
+ assertEquals("A", result[0].name)
+ assertEquals("B", result[1].name)
+ }
+
+ @Test
+ fun merge_appends_all_incoming_channels_in_order() {
+ val incoming = listOf(ChannelSettings(name = "C"), ChannelSettings(name = "D"))
+
+ val result = mergeChannelSettingsForAdd(existing = emptyList(), incoming)
+
+ assertEquals(2, result.size)
+ assertEquals("C", result[0].name)
+ assertEquals("D", result[1].name)
+ }
+
+ @Test
+ fun merge_preserves_structurally_equal_channels() {
+ val channel = ChannelSettings(name = "LongFast", psk = byteArrayOf(1).toByteString())
+
+ val result = mergeChannelSettingsForAdd(existing = listOf(channel), incoming = listOf(channel))
+
+ assertEquals(2, result.size)
+ }
+
+ @Test
+ fun merge_preserves_same_name_different_psk() {
+ val existingChan = ChannelSettings(name = "A", psk = byteArrayOf(1).toByteString())
+ val incomingChan = ChannelSettings(name = "A", psk = byteArrayOf(2).toByteString())
+
+ val result = mergeChannelSettingsForAdd(listOf(existingChan), listOf(incomingChan))
+
+ assertEquals(2, result.size)
+ }
+
+ @Test
+ fun merge_preserves_same_psk_different_name() {
+ val psk = byteArrayOf(1, 2).toByteString()
+ val existingChan = ChannelSettings(name = "A", psk = psk)
+ val incomingChan = ChannelSettings(name = "B", psk = psk)
+
+ val result = mergeChannelSettingsForAdd(listOf(existingChan), listOf(incomingChan))
+
+ assertEquals(2, result.size)
+ }
+
+ @Test
+ fun merge_preserves_duplicate_inside_incoming() {
+ val a = ChannelSettings(name = "A", psk = byteArrayOf(1).toByteString())
+ val b = ChannelSettings(name = "B", psk = byteArrayOf(2).toByteString())
+
+ val result = mergeChannelSettingsForAdd(existing = emptyList(), incoming = listOf(a, a, b))
+
+ assertEquals(3, result.size)
+ }
+
+ @Test
+ fun merge_both_empty_produces_empty_list() {
+ val result = mergeChannelSettingsForAdd(existing = emptyList(), incoming = emptyList())
+
+ assertTrue(result.isEmpty())
+ }
}

Served by rngit 1.5.2 - Generated in 0.08s